/*
 * SonarQube JavaScript Plugin
 * Copyright (C) 2011-2025 SonarSource Sàrl
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.plugins.javascript.bridge;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.sonar.plugins.javascript.api.estree.ESTree;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayElement;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ArrayPattern;
import org.sonar.plugins.javascript.bridge.protobuf.AssignmentExpression;
import org.sonar.plugins.javascript.bridge.protobuf.AssignmentPattern;
import org.sonar.plugins.javascript.bridge.protobuf.BinaryExpression;
import org.sonar.plugins.javascript.bridge.protobuf.BlockStatement;
import org.sonar.plugins.javascript.bridge.protobuf.CallExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ChainExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ClassDeclaration;
import org.sonar.plugins.javascript.bridge.protobuf.EmptyStatement;
import org.sonar.plugins.javascript.bridge.protobuf.ExportAllDeclaration;
import org.sonar.plugins.javascript.bridge.protobuf.ExportAssignment;
import org.sonar.plugins.javascript.bridge.protobuf.ExportDefaultDeclaration;
import org.sonar.plugins.javascript.bridge.protobuf.ExportSpecifier;
import org.sonar.plugins.javascript.bridge.protobuf.ExpressionStatement;
import org.sonar.plugins.javascript.bridge.protobuf.ImportDefaultSpecifier;
import org.sonar.plugins.javascript.bridge.protobuf.ImportExpression;
import org.sonar.plugins.javascript.bridge.protobuf.ImportSpecifier;
import org.sonar.plugins.javascript.bridge.protobuf.JSXAttribute;
import org.sonar.plugins.javascript.bridge.protobuf.JSXClosingElement;
import org.sonar.plugins.javascript.bridge.protobuf.JSXElement;
import org.sonar.plugins.javascript.bridge.protobuf.JSXExpressionContainer;
import org.sonar.plugins.javascript.bridge.protobuf.JSXFragment;
import org.sonar.plugins.javascript.bridge.protobuf.JSXIdentifier;
import org.sonar.plugins.javascript.bridge.protobuf.JSXMemberExpression;
import org.sonar.plugins.javascript.bridge.protobuf.JSXNamespacedName;
import org.sonar.plugins.javascript.bridge.protobuf.JSXOpeningElement;
import org.sonar.plugins.javascript.bridge.protobuf.JSXSpreadAttribute;
import org.sonar.plugins.javascript.bridge.protobuf.JSXSpreadChild;
import org.sonar.plugins.javascript.bridge.protobuf.JSXText;
import org.sonar.plugins.javascript.bridge.protobuf.Literal;
import org.sonar.plugins.javascript.bridge.protobuf.LogicalExpression;
import org.sonar.plugins.javascript.bridge.protobuf.Node;
import org.sonar.plugins.javascript.bridge.protobuf.NodeType;
import org.sonar.plugins.javascript.bridge.protobuf.Position;
import org.sonar.plugins.javascript.bridge.protobuf.Program;
import org.sonar.plugins.javascript.bridge.protobuf.SourceLocation;
import org.sonar.plugins.javascript.bridge.protobuf.StaticBlock;
import org.sonar.plugins.javascript.bridge.protobuf.TSExternalModuleReference;
import org.sonar.plugins.javascript.bridge.protobuf.TSImportEqualsDeclaration;
import org.sonar.plugins.javascript.bridge.protobuf.TSModuleBlock;
import org.sonar.plugins.javascript.bridge.protobuf.TSModuleDeclaration;
import org.sonar.plugins.javascript.bridge.protobuf.TSParameterProperty;
import org.sonar.plugins.javascript.bridge.protobuf.TSQualifiedName;
import org.sonar.plugins.javascript.bridge.protobuf.UnaryExpression;
import org.sonar.plugins.javascript.bridge.protobuf.UpdateExpression;
import org.sonar.plugins.javascript.bridge.protobuf.WithStatement;

class ESTreeFactoryTest {

  @Test
  void should_not_fail_with_unknown_nodes() throws IOException {
    // the clear version of serialized.proto is `packages/jsts/tests/parsers/fixtures/ast/unknownNode.ts`,
    // it was generated by writing to a file the serialized data in the test `packages/jsts/tests/parsers/ast.test.ts`
    File file = Path.of("src", "test", "resources", "files", "unknown.proto").toFile();

    Node node;
    try (FileInputStream fis = new FileInputStream(file)) {
      node = Node.parseFrom(fis);
    }
    ESTree.Node root = ESTreeFactory.from(node, ESTree.Node.class);
    assertThat(root).isInstanceOf(ESTree.Program.class);
    ESTree.Program program = (ESTree.Program) root;
    assertThat(program.body()).hasSize(1);
    assertThat(program.body().get(0)).isInstanceOfSatisfying(
      ESTree.IfStatement.class,
      ifStatement -> {
        assertThat(ifStatement.test()).isInstanceOf(ESTree.UnknownNode.class);
      }
    );
  }

  @Test
  void should_create_nodes_from_serialized_data() throws IOException {
    // the clear version of serialized.proto is `packages/jsts/tests/parsers/fixtures/ast/base.js`,
    // it was generated by writing to a file the serialized data in the test `packages/jsts/tests/parsers/ast.test.ts`
    File file = Path.of("src", "test", "resources", "files", "serialized.proto").toFile();

    Node node;
    try (FileInputStream fis = new FileInputStream(file)) {
      node = Node.parseFrom(fis);
    }
    ESTree.Node root = ESTreeFactory.from(node, ESTree.Node.class);
    assertThat(root).isInstanceOf(ESTree.Program.class);
    ESTree.Program program = (ESTree.Program) root;
    assertThat(program.body()).hasSize(55);
    // Assert a few nodes.
    assertThat(program.body().get(0)).isInstanceOfSatisfying(
      ESTree.VariableDeclaration.class,
      variableDeclaration -> {
        assertThat(variableDeclaration.declarations()).hasSize(1);
        assertThat(variableDeclaration.kind()).isEqualTo("let");
        ESTree.VariableDeclarator variableDeclarator = variableDeclaration.declarations().get(0);
        assertThat(variableDeclarator.id()).isInstanceOf(ESTree.Identifier.class);
        assertThat(variableDeclarator.init()).contains(
          new ESTree.SimpleLiteral(
            new ESTree.Location(new ESTree.Position(20, 8), new ESTree.Position(20, 12)),
            "",
            "null"
          )
        );
      }
    );
    assertThat(program.body().get(14)).isInstanceOfSatisfying(
      ESTree.IfStatement.class,
      ifStatement -> {
        assertThat(ifStatement.test()).isInstanceOf(ESTree.Identifier.class);
        assertThat(ifStatement.consequent()).isInstanceOf(ESTree.BlockStatement.class);
        assertThat(ifStatement.alternate()).isEmpty();
      }
    );

    // Source code: [, , 5]
    assertThat(program.body().get(42)).isInstanceOfSatisfying(
      ESTree.VariableDeclaration.class,
      declaration -> {
        Optional<ESTree.Expression> initializer = declaration.declarations().get(0).init();
        assertThat(initializer).isPresent();
        assertThat(initializer.get()).isInstanceOfSatisfying(
          ESTree.ArrayExpression.class,
          arrayExpression -> {
            assertThat(arrayExpression.elements()).hasSize(3);
            assertThat(arrayExpression.elements().get(0)).isEmpty();
            assertThat(arrayExpression.elements().get(1)).isEmpty();
            assertThat(arrayExpression.elements().get(2)).isNotEmpty();
          }
        );
      }
    );
  }

  @Test
  void should_create_program() {
    Node body = Node.newBuilder()
      .setType(NodeType.BlockStatementType)
      .setBlockStatement(BlockStatement.newBuilder().build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ProgramType)
      .setProgram(Program.newBuilder().setSourceType("script").addBody(body).build())
      .setLoc(
        SourceLocation.newBuilder()
          .setStart(Position.newBuilder().setLine(1).setColumn(2).build())
          .setEnd(Position.newBuilder().setLine(3).setColumn(4).build())
          .build()
      )
      .build();

    ESTree.Program estreeProgram = ESTreeFactory.from(protobufNode, ESTree.Program.class);
    assertThat(estreeProgram.sourceType()).isEqualTo("script");
    assertThat(estreeProgram.loc().start().line()).isEqualTo(1);
    assertThat(estreeProgram.loc().start().column()).isEqualTo(2);
    assertThat(estreeProgram.loc().end().line()).isEqualTo(3);
    assertThat(estreeProgram.loc().end().column()).isEqualTo(4);
    assertThat(estreeProgram.body()).hasSize(1);
    ESTree.Node estreeBody = estreeProgram.body().get(0);
    assertThat(estreeBody).isInstanceOfSatisfying(ESTree.BlockStatement.class, blockStatement ->
      assertThat(blockStatement.body()).isEmpty()
    );
  }

  @Test
  void should_create_expression_statement_when_directive_is_empty() {
    Node expressionContent = Node.newBuilder().setType(NodeType.ThisExpressionType).build();
    ExpressionStatement expressionStatement = ExpressionStatement.newBuilder()
      .setExpression(expressionContent)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ExpressionStatementType)
      .setExpressionStatement(expressionStatement)
      .build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOf(ESTree.ExpressionStatement.class);
  }

  @Test
  void should_create_directive_from_expression_statement() {
    Node expressionContent = Node.newBuilder().setType(NodeType.LiteralType).build();
    ExpressionStatement expressionStatement = ExpressionStatement.newBuilder()
      .setDirective("directive")
      .setExpression(expressionContent)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ExpressionStatementType)
      .setExpressionStatement(expressionStatement)
      .build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.Directive.class,
      directive -> assertThat(directive.directive()).isEqualTo("directive")
    );
  }

  @Test
  void should_create_BigIntLiteral() {
    Literal literal = Literal.newBuilder().setBigint("1234").build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.BigIntLiteral.class,
      bigIntLiteral -> {
        assertThat(bigIntLiteral.bigint()).isEqualTo("1234");
        assertThat(bigIntLiteral.value()).isEqualTo(new BigInteger("1234"));
        // Default value.
        assertThat(bigIntLiteral.raw()).isEmpty();
      }
    );
  }

  @Test
  void should_create_simple_string_literal() {
    Literal literal = Literal.newBuilder().setRaw("'raw'").setValueString("raw").build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.SimpleLiteral.class,
      simpleLiteral -> {
        assertThat(simpleLiteral.raw()).isEqualTo("'raw'");
        assertThat(simpleLiteral.value()).isEqualTo("raw");
      }
    );
  }

  @Test
  void should_create_simple_int_literal() {
    Literal literal = Literal.newBuilder().setRaw("42").setValueNumber(42).build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.SimpleLiteral.class,
      simpleLiteral -> {
        assertThat(simpleLiteral.raw()).isEqualTo("42");
        assertThat(simpleLiteral.value()).isEqualTo(42);
      }
    );
  }

  @Test
  void should_create_simple_bool_literal() {
    Literal literal = Literal.newBuilder().setRaw("true").setValueBoolean(true).build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.SimpleLiteral.class,
      simpleLiteral -> {
        assertThat(simpleLiteral.raw()).isEqualTo("true");
        assertThat(simpleLiteral.value()).isEqualTo(true);
      }
    );
  }

  @Test
  void should_create_reg_exp_literal() {
    Literal literal = Literal.newBuilder().setPattern("1234").build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.RegExpLiteral.class,
      regExpLiteral -> {
        assertThat(regExpLiteral.pattern()).isEqualTo("1234");
        assertThat(regExpLiteral.flags()).isEmpty();
        // Default value.
        assertThat(regExpLiteral.raw()).isEmpty();
      }
    );
  }

  @Test
  void should_create_reg_exp_literal_with_flag() {
    Literal literal = Literal.newBuilder().setPattern("1234").setFlags("flag").build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.RegExpLiteral.class,
      regExpLiteral -> {
        assertThat(regExpLiteral.pattern()).isEqualTo("1234");
        assertThat(regExpLiteral.flags()).isEqualTo("flag");
        // Default value.
        assertThat(regExpLiteral.raw()).isEmpty();
      }
    );
  }

  @Test
  void should_create_simple_null_literal() {
    // Null literal is represented as a raw value "null" in protobuf.
    // The field "value" will not be set, resulting in an empty string.
    Literal literal = Literal.newBuilder().setRaw("null").build();
    Node protobufNode = Node.newBuilder().setType(NodeType.LiteralType).setLiteral(literal).build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.SimpleLiteral.class,
      simpleLiteral -> {
        assertThat(simpleLiteral.raw()).isEqualTo("null");
        assertThat(simpleLiteral.value()).isEqualTo("");
      }
    );
  }

  @Test
  void should_create_simple_call_expression() {
    CallExpression callExpression = CallExpression.newBuilder()
      .setCallee(Node.newBuilder().setType(NodeType.SuperType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.CallExpressionType)
      .setCallExpression(callExpression)
      .build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.CallExpression.class,
      estreeCallExpression -> {
        assertThat(estreeCallExpression.callee()).isInstanceOf(ESTree.Super.class);
        assertThat(estreeCallExpression.arguments()).isEmpty();
      }
    );
  }

  @Test
  void should_create_binary_expression() {
    BinaryExpression binaryExpression = BinaryExpression.newBuilder()
      .setOperator("-")
      .setLeft(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .setRight(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.BinaryExpressionType)
      .setBinaryExpression(binaryExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.BinaryExpression.class, binary -> {
      assertThat(binary.left()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(binary.right()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(binary.operator()).isEqualTo(ESTree.BinaryOperator.MINUS);
    });
  }

  @Test
  void should_create_unary_expression() {
    UnaryExpression binaryExpression = UnaryExpression.newBuilder()
      .setOperator("!")
      .setArgument(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .setPrefix(true)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.UnaryExpressionType)
      .setUnaryExpression(binaryExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.UnaryExpression.class, unary -> {
      assertThat(unary.argument()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(unary.prefix()).isTrue();
      assertThat(unary.operator()).isEqualTo(ESTree.UnaryOperator.LOGICAL_NOT);
    });
  }

  @Test
  void should_create_logical_expression() {
    LogicalExpression logicalExpression = LogicalExpression.newBuilder()
      .setOperator("&&")
      .setLeft(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .setRight(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.LogicalExpressionType)
      .setLogicalExpression(logicalExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.LogicalExpression.class, logical -> {
      assertThat(logical.left()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(logical.right()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(logical.operator()).isEqualTo(ESTree.LogicalOperator.AND);
    });
  }

  @Test
  void should_create_assignment_expression() {
    AssignmentExpression assignmentExpression = AssignmentExpression.newBuilder()
      .setOperator(">>>=")
      .setLeft(Node.newBuilder().setType(NodeType.ArrayPatternType).build())
      .setRight(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.AssignmentExpressionType)
      .setAssignmentExpression(assignmentExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.AssignmentExpression.class, logical -> {
      assertThat(logical.left()).isInstanceOf(ESTree.ArrayPattern.class);
      assertThat(logical.right()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(logical.operator()).isEqualTo(
        ESTree.AssignmentOperator.UNSIGNED_RIGHT_SHIFT_ASSIGN
      );
    });
  }

  @Test
  void should_create_update_expression() {
    UpdateExpression updateExpression = UpdateExpression.newBuilder()
      .setOperator("--")
      .setArgument(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.UpdateExpressionType)
      .setUpdateExpression(updateExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.UpdateExpression.class, logical -> {
      assertThat(logical.argument()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(logical.prefix()).isFalse();
      assertThat(logical.operator()).isEqualTo(ESTree.UpdateOperator.DECREMENT);
    });
  }

  @Test
  void should_create_export_default_declaration() {
    ClassDeclaration classDeclaration = ClassDeclaration.newBuilder()
      .setBody(Node.newBuilder().setType(NodeType.ClassBodyType).build())
      .build();
    Node classDeclarationNode = Node.newBuilder()
      .setType(NodeType.ClassDeclarationType)
      .setClassDeclaration(classDeclaration)
      .build();
    ExportDefaultDeclaration declaration = ExportDefaultDeclaration.newBuilder()
      .setDeclaration(classDeclarationNode)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ExportDefaultDeclarationType)
      .setExportDefaultDeclaration(declaration)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ExportDefaultDeclaration.class, export -> {
      assertThat(export.declaration()).isInstanceOf(ESTree.ClassDeclaration.class);
    });
  }

  @Test
  void should_create_assignment_pattern() {
    AssignmentPattern assignmentPattern = AssignmentPattern.newBuilder()
      .setLeft(Node.newBuilder().setType(NodeType.ArrayPatternType).build())
      .setRight(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.AssignmentPatternType)
      .setAssignmentPattern(assignmentPattern)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.AssignmentPattern.class, pattern -> {
      assertThat(pattern.left()).isInstanceOf(ESTree.ArrayPattern.class);
      assertThat(pattern.right()).isInstanceOf(ESTree.ThisExpression.class);
    });
  }

  @Test
  void should_create_import_expression() {
    ImportExpression importExpression = ImportExpression.newBuilder()
      .setSource(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ImportExpressionType)
      .setImportExpression(importExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ImportExpression.class, expression ->
      assertThat(expression.source()).isInstanceOf(ESTree.ThisExpression.class)
    );
  }

  @Test
  void should_create_export_specifier_type() {
    ExportSpecifier exportSpecifier = ExportSpecifier.newBuilder()
      .setLocal(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setExported(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ExportSpecifierType)
      .setExportSpecifier(exportSpecifier)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.ExportSpecifier.class);
  }

  @Test
  void should_create_import_default_specifier_type() {
    ImportDefaultSpecifier importDefaultSpecifier = ImportDefaultSpecifier.newBuilder()
      .setLocal(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ImportDefaultSpecifierType)
      .setImportDefaultSpecifier(importDefaultSpecifier)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.ImportDefaultSpecifier.class);
  }

  @Test
  void should_create_import_specifier_type() {
    ImportSpecifier importSpecifier = ImportSpecifier.newBuilder()
      .setLocal(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setImported(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ImportSpecifierType)
      .setImportSpecifier(importSpecifier)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.ImportSpecifier.class);
  }

  @Test
  void should_create_chain_expression_type() {
    CallExpression callExpression = CallExpression.newBuilder()
      .setCallee(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .build();
    Node chainElement = Node.newBuilder()
      .setType(NodeType.CallExpressionType)
      .setCallExpression(callExpression)
      .build();
    ChainExpression chainExpression = ChainExpression.newBuilder()
      .setExpression(chainElement)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ChainExpressionType)
      .setChainExpression(chainExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ChainExpression.class, chain -> {
      assertThat(chain.expression()).isInstanceOf(ESTree.CallExpression.class);
    });
  }

  @Test
  void should_create_with_statement_type() {
    WithStatement withStatement = WithStatement.newBuilder()
      .setObject(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
      .setBody(Node.newBuilder().setType(NodeType.BlockStatementType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.WithStatementType)
      .setWithStatement(withStatement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.WithStatement.class, with -> {
      assertThat(with.object()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(with.body()).isInstanceOf(ESTree.BlockStatement.class);
    });
  }

  @Test
  void should_create_empty_statement_type() {
    EmptyStatement emptyStatement = EmptyStatement.newBuilder().build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.EmptyStatementType)
      .setEmptyStatement(emptyStatement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.EmptyStatement.class);
  }

  @Test
  void should_create_static_block_type() {
    StaticBlock staticBlock = StaticBlock.newBuilder()
      .addBody(Node.newBuilder().setType(NodeType.EmptyStatementType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.StaticBlockType)
      .setStaticBlock(staticBlock)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.StaticBlock.class, block -> {
      assertThat(block.body().size()).isEqualTo(1);
      assertThat(block.body().get(0)).isInstanceOf(ESTree.EmptyStatement.class);
    });
  }

  @Test
  void should_create_export_all_declaration_type_using_a_literal() {
    ExportAllDeclaration exportAllDeclaration = ExportAllDeclaration.newBuilder()
      .setExported(
        Node.newBuilder()
          .setType(NodeType.LiteralType)
          .setLiteral(Literal.newBuilder().setValueString("4k"))
          .build()
      )
      .setSource(
        Node.newBuilder()
          .setType(NodeType.LiteralType)
          .setLiteral(Literal.newBuilder().setValueString("yolo"))
      )
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ExportAllDeclarationType)
      .setExportAllDeclaration(exportAllDeclaration)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ExportAllDeclaration.class, declaration -> {
      assertThat(declaration.exported().isPresent()).isTrue();
      var exported = declaration.exported().get();
      assertThat(exported).isInstanceOf(ESTree.Literal.class);
    });
  }

  @Test
  void directive_can_be_in_block_statement() {
    BlockStatement blockStatement = BlockStatement.newBuilder()
      .addBody(
        Node.newBuilder()
          .setType(NodeType.ExpressionStatementType)
          .setExpressionStatement(
            ExpressionStatement.newBuilder()
              .setDirective("directiveName")
              .setExpression(Node.newBuilder().setType(NodeType.LiteralType).build())
              .build()
          )
          .build()
      )
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.BlockStatementType)
      .setBlockStatement(blockStatement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.BlockStatement.class, block -> {
      assertThat(block.body()).hasSize(1);
      assertThat(block.body().get(0)).isInstanceOfSatisfying(ESTree.Directive.class, directive -> {
        assertThat(directive.directive()).isEqualTo("directiveName");
      });
    });
  }

  @Test
  void array_expression_can_contain_empty_elements() {
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ArrayExpressionType)
      .setArrayExpression(
        ArrayExpression.newBuilder()
          .addElements(ArrayElement.newBuilder().build())
          .addElements(
            ArrayElement.newBuilder()
              .setElement(Node.newBuilder().setType(NodeType.ThisExpressionType).build())
              .build()
          )
          .addElements(ArrayElement.newBuilder().build())
          .build()
      )
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ArrayExpression.class, array -> {
      assertThat(array.elements()).hasSize(3);
      assertThat(array.elements().get(0)).isEmpty();
      assertThat(array.elements().get(1).get()).isInstanceOf(ESTree.ThisExpression.class);
      assertThat(array.elements().get(2)).isEmpty();
    });
  }

  @Test
  void array_pattern_can_contain_empty_elements() {
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.ArrayPatternType)
      .setArrayPattern(
        ArrayPattern.newBuilder()
          .addElements(ArrayElement.newBuilder().build())
          .addElements(
            ArrayElement.newBuilder()
              .setElement(Node.newBuilder().setType(NodeType.IdentifierType).build())
              .build()
          )
          .addElements(ArrayElement.newBuilder().build())
          .build()
      )
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.ArrayPattern.class, array -> {
      assertThat(array.elements()).hasSize(3);
      assertThat(array.elements().get(0)).isEmpty();
      assertThat(array.elements().get(1).get()).isInstanceOf(ESTree.Identifier.class);
      assertThat(array.elements().get(2)).isEmpty();
    });
  }

  @Test
  void should_create_export_assignment() {
    CallExpression callExpression = CallExpression.newBuilder()
      .setCallee(Node.newBuilder().setType(NodeType.SuperType).build())
      .build();
    Node node = Node.newBuilder()
      .setType(NodeType.CallExpressionType)
      .setCallExpression(callExpression)
      .build();
    //
    ExportAssignment exportAssignment = ExportAssignment.newBuilder().setExpression(node).build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSExportAssignmentType)
      .setExportAssignment(exportAssignment)
      .build();

    ESTree.Node estreeExpressionStatement = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeExpressionStatement).isInstanceOfSatisfying(
      ESTree.ExportAssignment.class,
      export -> {
        assertThat(export.expression()).isInstanceOf(ESTree.CallExpression.class);
      }
    );
  }

  @Test
  void should_create_ts_qualified_name() {
    TSQualifiedName innerTsqn = TSQualifiedName.newBuilder()
      .setLeft(Node.newBuilder().setType(NodeType.IdentifierType))
      .setRight(Node.newBuilder().setType(NodeType.IdentifierType))
      .build();

    Node innerTsqnNode = Node.newBuilder()
      .setType(NodeType.TSQualifiedNameType)
      .setTSQualifiedName(innerTsqn)
      .build();

    TSQualifiedName tsQualifiedName = TSQualifiedName.newBuilder()
      .setLeft(innerTsqnNode)
      .setRight(Node.newBuilder().setType(NodeType.IdentifierType))
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSQualifiedNameType)
      .setTSQualifiedName(tsQualifiedName)
      .build();

    ESTree.Node estreeTsQualifiedName = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estreeTsQualifiedName).isInstanceOfSatisfying(ESTree.TSQualifiedName.class, tsqn -> {
      assertThat(tsqn.left()).isInstanceOfSatisfying(ESTree.TSQualifiedName.class, left -> {
        assertThat(left.left()).isInstanceOf(ESTree.Identifier.class);
        assertThat(left.right()).isInstanceOf(ESTree.Identifier.class);
      });
      assertThat(tsqn.right()).isInstanceOf(ESTree.Identifier.class);
    });
  }

  @Test
  void should_create_ts_external_module_reference() {
    TSExternalModuleReference tsExternalModuleReference = TSExternalModuleReference.newBuilder()
      .setExpression(Node.newBuilder().setType(NodeType.LiteralType))
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSExternalModuleReferenceType)
      .setTSExternalModuleReference(tsExternalModuleReference)
      .build();

    ESTree.Node estreeTsExternalModuleReference = ESTreeFactory.from(
      protobufNode,
      ESTree.Node.class
    );
    assertThat(estreeTsExternalModuleReference).isInstanceOfSatisfying(
      ESTree.TSExternalModuleReference.class,
      tsemr -> assertThat(tsemr.expression()).isInstanceOf(ESTree.Literal.class)
    );
  }

  @Test
  void should_create_ts_import_equals_declaration() {
    TSImportEqualsDeclaration tsImportEqualsDeclaration = TSImportEqualsDeclaration.newBuilder()
      .setId(Node.newBuilder().setType(NodeType.IdentifierType))
      .setModuleReference(Node.newBuilder().setType(NodeType.IdentifierType))
      .setImportKind("value")
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSImportEqualsDeclarationType)
      .setTSImportEqualsDeclaration(tsImportEqualsDeclaration)
      .build();

    ESTree.Node estreeTsImportEqualsDeclaration = ESTreeFactory.from(
      protobufNode,
      ESTree.Node.class
    );
    assertThat(estreeTsImportEqualsDeclaration).isInstanceOfSatisfying(
      ESTree.TSImportEqualsDeclaration.class,
      tsied -> {
        assertThat(tsied.id()).isInstanceOf(ESTree.Identifier.class);
        assertThat(tsied.moduleReference()).isInstanceOf(ESTree.Identifier.class);
        assertThat(tsied.importKind()).isInstanceOf(String.class);
      }
    );
  }

  @Test
  void should_create_ts_module_block() {
    Node emptyStatementNode = Node.newBuilder()
      .setType(NodeType.EmptyStatementType)
      .setEmptyStatement(EmptyStatement.newBuilder().build())
      .build();
    TSModuleBlock tsModuleBlock = TSModuleBlock.newBuilder().addBody(emptyStatementNode).build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSModuleBlockType)
      .setTSModuleBlock(tsModuleBlock)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleBlock.class, block -> {
      assertThat(block.body()).hasSize(1);
      assertThat(block.body().get(0)).isInstanceOf(ESTree.EmptyStatement.class);
    });
  }

  @Test
  void should_create_ts_module_declaration() {
    TSModuleDeclaration tsModuleDeclaration = TSModuleDeclaration.newBuilder()
      .setId(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setBody(Node.newBuilder().setType(NodeType.TSModuleBlockType).build())
      .setKind("module")
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSModuleDeclarationType)
      .setTSModuleDeclaration(tsModuleDeclaration)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleDeclaration.class, module -> {
      assertThat(module.id()).isInstanceOf(ESTree.Identifier.class);
      assertThat(module.body()).isPresent();
      assertThat(module.body().get()).isInstanceOf(ESTree.TSModuleBlock.class);
      assertThat(module.kind()).isEqualTo("module");
    });
  }

  @Test
  void should_create_ts_module_declaration_without_body() {
    TSModuleDeclaration tsModuleDeclaration = TSModuleDeclaration.newBuilder()
      .setId(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setKind("module")
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSModuleDeclarationType)
      .setTSModuleDeclaration(tsModuleDeclaration)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.TSModuleDeclaration.class, module -> {
      assertThat(module.id()).isInstanceOf(ESTree.Identifier.class);
      assertThat(module.body()).isEmpty();
      assertThat(module.kind()).isEqualTo("module");
    });
  }

  @Test
  void should_create_ts_parameter_property() {
    TSParameterProperty tsParameterProperty = TSParameterProperty.newBuilder()
      .setParameter(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setReadonly(true)
      .setAccessibility("public")
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSParameterPropertyType)
      .setTSParameterProperty(tsParameterProperty)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.TSParameterProperty.class, param -> {
      assertThat(param.parameter()).isInstanceOf(ESTree.Identifier.class);
      assertThat(param.readonly()).isTrue();
      assertThat(param.accessibility()).contains("public");
    });
  }

  @Test
  void should_create_ts_parameter_property_with_empty_accessibility() {
    TSParameterProperty tsParameterProperty = TSParameterProperty.newBuilder()
      .setParameter(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .setReadonly(true)
      .build();

    Node protobufNode = Node.newBuilder()
      .setType(NodeType.TSParameterPropertyType)
      .setTSParameterProperty(tsParameterProperty)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.TSParameterProperty.class, param -> {
      assertThat(param.parameter()).isInstanceOf(ESTree.Identifier.class);
      assertThat(param.readonly()).isTrue();
      assertThat(param.accessibility()).isEmpty();
    });
  }

  @Test
  void throw_exception_from_unrecognized_type() {
    Node protobufNode = Node.newBuilder().setTypeValue(-1).build();

    assertThatThrownBy(() -> ESTreeFactory.from(protobufNode, ESTree.Node.class))
      .isInstanceOf(IllegalArgumentException.class)
      .hasMessageStartingWith("Unknown node type: UNRECOGNIZED");
  }

  @Test
  void throw_exception_for_incorrect_cast() {
    Node block = Node.newBuilder()
      .setType(NodeType.BlockStatementType)
      .setBlockStatement(BlockStatement.newBuilder().build())
      .build();

    assertThatThrownBy(() -> ESTreeFactory.from(block, ESTree.Super.class))
      .isInstanceOf(IllegalStateException.class)
      .hasMessage(
        "Expected class org.sonar.plugins.javascript.api.estree.ESTree$Super " +
          "but got class org.sonar.plugins.javascript.api.estree.ESTree$BlockStatement"
      );
  }

  @ParameterizedTest
  @MethodSource("provideInputsForExpectedJavaTypes")
  <T> void node_type_should_create_expected_java_types(NodeType nodeType, Class<T> clazz) {
    assertNodeTypeIsParsedToExpectedClass(nodeType, clazz);
  }

  private static Stream<Arguments> provideInputsForExpectedJavaTypes() {
    return Stream.of(
      Arguments.of(
        NodeType.TSAbstractMethodDefinitionType,
        ESTree.TSAbstractMethodDefinition.class
      ),
      Arguments.of(
        NodeType.TSAbstractMethodDefinitionType,
        ESTree.TSAbstractMethodDefinition.class
      ),
      Arguments.of(NodeType.TSDeclareFunctionType, ESTree.TSDeclareFunction.class),
      Arguments.of(NodeType.TSInterfaceDeclarationType, ESTree.TSInterfaceDeclaration.class),
      Arguments.of(NodeType.TSEnumDeclarationType, ESTree.TSEnumDeclaration.class),
      Arguments.of(NodeType.TSTypeAliasDeclarationType, ESTree.TSTypeAliasDeclaration.class),
      Arguments.of(
        NodeType.TSEmptyBodyFunctionExpressionType,
        ESTree.TSEmptyBodyFunctionExpression.class
      )
    );
  }

  private static <T> void assertNodeTypeIsParsedToExpectedClass(NodeType nodeType, Class<T> clazz) {
    Node tsTypeAliasDeclaration = Node.newBuilder().setType(nodeType).build();
    assertThat(ESTreeFactory.from(tsTypeAliasDeclaration, ESTree.Node.class)).isInstanceOf(clazz);
  }

  @Test
  void should_create_jsx_identifier() {
    // <MyComponent/> - the `MyComponent` part
    JSXIdentifier jsxIdentifier = JSXIdentifier.newBuilder().setName("MyComponent").build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXIdentifierType)
      .setJSXIdentifier(jsxIdentifier)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXIdentifier.class, jsx -> {
      assertThat(jsx.name()).isEqualTo("MyComponent");
    });
  }

  @Test
  void should_create_jsx_text() {
    // <h1>Hello, World!</h1> - the `Hello, World!` part
    JSXText jsxText = JSXText.newBuilder()
      .setRaw("Hello, World!")
      .setValue("Hello, World!")
      .build();
    Node protobufNode = Node.newBuilder().setType(NodeType.JSXTextType).setJSXText(jsxText).build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXText.class, jsx -> {
      assertThat(jsx.raw()).isEqualTo("Hello, World!");
      assertThat(jsx.value()).isEqualTo("Hello, World!");
    });
  }

  @Test
  void should_create_jsx_empty_expression() {
    // <div>{}</div> - the `{}` part
    Node protobufNode = Node.newBuilder().setType(NodeType.JSXEmptyExpressionType).build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.JSXEmptyExpression.class);
  }

  @Test
  void should_create_jsx_opening_fragment() {
    // <>...</> - the `<>` part
    Node protobufNode = Node.newBuilder().setType(NodeType.JSXOpeningFragmentType).build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.JSXOpeningFragment.class);
  }

  @Test
  void should_create_jsx_closing_fragment() {
    // <>...</> - the `</>` part
    Node protobufNode = Node.newBuilder().setType(NodeType.JSXClosingFragmentType).build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOf(ESTree.JSXClosingFragment.class);
  }

  @Test
  void should_create_jsx_expression_container() {
    // <div>{value}</div> - the `{value}` part
    JSXExpressionContainer jsxExpressionContainer = JSXExpressionContainer.newBuilder()
      .setExpression(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXExpressionContainerType)
      .setJSXExpressionContainer(jsxExpressionContainer)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXExpressionContainer.class, jsx -> {
      assertThat(jsx.expression()).isInstanceOf(ESTree.Identifier.class);
    });
  }

  @Test
  void should_create_jsx_spread_child() {
    // <div>{...children}</div> - the `{...children}` part
    JSXSpreadChild jsxSpreadChild = JSXSpreadChild.newBuilder()
      .setExpression(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXSpreadChildType)
      .setJSXSpreadChild(jsxSpreadChild)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXSpreadChild.class, jsx -> {
      assertThat(jsx.expression()).isInstanceOf(ESTree.Identifier.class);
    });
  }

  @Test
  void should_create_jsx_spread_attribute() {
    // <div {...props}/> - the `{...props}` part
    JSXSpreadAttribute jsxSpreadAttribute = JSXSpreadAttribute.newBuilder()
      .setArgument(Node.newBuilder().setType(NodeType.IdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXSpreadAttributeType)
      .setJSXSpreadAttribute(jsxSpreadAttribute)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXSpreadAttribute.class, jsx -> {
      assertThat(jsx.argument()).isInstanceOf(ESTree.Identifier.class);
    });
  }

  @Test
  void should_create_jsx_member_expression() {
    // <Foo.Bar/> - the `Foo.Bar` part
    JSXMemberExpression jsxMemberExpression = JSXMemberExpression.newBuilder()
      .setObject(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .setProperty(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXMemberExpressionType)
      .setJSXMemberExpression(jsxMemberExpression)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXMemberExpression.class, jsx -> {
      assertThat(jsx.object()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.property()).isInstanceOf(ESTree.JSXIdentifier.class);
    });
  }

  @Test
  void should_create_jsx_namespaced_name() {
    // <foo:bar/> - the `foo:bar` part
    JSXNamespacedName jsxNamespacedName = JSXNamespacedName.newBuilder()
      .setNamespace(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXNamespacedNameType)
      .setJSXNamespacedName(jsxNamespacedName)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXNamespacedName.class, jsx -> {
      assertThat(jsx.namespace()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
    });
  }

  @Test
  void should_create_jsx_attribute_with_value() {
    // <div id="test"/> - the `id="test"` part
    JSXAttribute jsxAttribute = JSXAttribute.newBuilder()
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .setValue(Node.newBuilder().setType(NodeType.LiteralType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXAttributeType)
      .setJSXAttribute(jsxAttribute)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXAttribute.class, jsx -> {
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.value()).isPresent();
      assertThat(jsx.value().get()).isInstanceOf(ESTree.Literal.class);
    });
  }

  @Test
  void should_create_jsx_attribute_without_value() {
    // <input disabled/> - the `disabled` part
    JSXAttribute jsxAttribute = JSXAttribute.newBuilder()
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXAttributeType)
      .setJSXAttribute(jsxAttribute)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXAttribute.class, jsx -> {
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.value()).isEmpty();
    });
  }

  @Test
  void should_create_jsx_closing_element() {
    // <h1>...</h1> - the `</h1>` part
    JSXClosingElement jsxClosingElement = JSXClosingElement.newBuilder()
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXClosingElementType)
      .setJSXClosingElement(jsxClosingElement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXClosingElement.class, jsx -> {
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
    });
  }

  @Test
  void should_create_jsx_opening_element() {
    // <h1>...</h1> - the `<h1>` part
    JSXOpeningElement jsxOpeningElement = JSXOpeningElement.newBuilder()
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .setSelfClosing(false)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXOpeningElementType)
      .setJSXOpeningElement(jsxOpeningElement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXOpeningElement.class, jsx -> {
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.selfClosing()).isFalse();
      assertThat(jsx.attributes()).isEmpty();
      assertThat(jsx.typeArguments()).isEmpty();
    });
  }

  @Test
  void should_create_jsx_opening_element_with_type_arguments() {
    // <Component<T> prop="value"/> - the `<Component<T> ...>` part
    JSXOpeningElement jsxOpeningElement = JSXOpeningElement.newBuilder()
      .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
      .setSelfClosing(true)
      .setTypeArguments(
        Node.newBuilder().setType(NodeType.TSTypeParameterInstantiationType).build()
      )
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXOpeningElementType)
      .setJSXOpeningElement(jsxOpeningElement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXOpeningElement.class, jsx -> {
      assertThat(jsx.name()).isInstanceOf(ESTree.JSXIdentifier.class);
      assertThat(jsx.selfClosing()).isTrue();
      assertThat(jsx.typeArguments()).isPresent();
      assertThat(jsx.typeArguments().get()).isInstanceOf(ESTree.TSTypeParameterInstantiation.class);
    });
  }

  @Test
  void should_create_jsx_element_with_closing_element() {
    // <h1>text</h1>
    Node openingElement = Node.newBuilder()
      .setType(NodeType.JSXOpeningElementType)
      .setJSXOpeningElement(
        JSXOpeningElement.newBuilder()
          .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
          .setSelfClosing(false)
          .build()
      )
      .build();
    Node closingElement = Node.newBuilder()
      .setType(NodeType.JSXClosingElementType)
      .setJSXClosingElement(
        JSXClosingElement.newBuilder()
          .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
          .build()
      )
      .build();
    Node textChild = Node.newBuilder()
      .setType(NodeType.JSXTextType)
      .setJSXText(JSXText.newBuilder().setRaw("text").setValue("text").build())
      .build();
    JSXElement jsxElement = JSXElement.newBuilder()
      .setOpeningElement(openingElement)
      .setClosingElement(closingElement)
      .addChildren(textChild)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXElementType)
      .setJSXElement(jsxElement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXElement.class, jsx -> {
      assertThat(jsx.openingElement()).isInstanceOf(ESTree.JSXOpeningElement.class);
      assertThat(jsx.closingElement()).isPresent();
      assertThat(jsx.closingElement().get()).isInstanceOf(ESTree.JSXClosingElement.class);
      assertThat(jsx.children()).hasSize(1);
      assertThat(jsx.children().get(0)).isInstanceOf(ESTree.JSXText.class);
    });
  }

  @Test
  void should_create_jsx_element_self_closing() {
    // <Component/>
    Node openingElement = Node.newBuilder()
      .setType(NodeType.JSXOpeningElementType)
      .setJSXOpeningElement(
        JSXOpeningElement.newBuilder()
          .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
          .setSelfClosing(true)
          .build()
      )
      .build();
    JSXElement jsxElement = JSXElement.newBuilder().setOpeningElement(openingElement).build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXElementType)
      .setJSXElement(jsxElement)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXElement.class, jsx -> {
      assertThat(jsx.openingElement()).isInstanceOf(ESTree.JSXOpeningElement.class);
      assertThat(jsx.closingElement()).isEmpty();
      assertThat(jsx.children()).isEmpty();
    });
  }

  @Test
  void should_create_jsx_fragment() {
    // <><Element/><Element/></>
    Node openingFragment = Node.newBuilder().setType(NodeType.JSXOpeningFragmentType).build();
    Node closingFragment = Node.newBuilder().setType(NodeType.JSXClosingFragmentType).build();
    Node childElement = Node.newBuilder()
      .setType(NodeType.JSXElementType)
      .setJSXElement(
        JSXElement.newBuilder()
          .setOpeningElement(
            Node.newBuilder()
              .setType(NodeType.JSXOpeningElementType)
              .setJSXOpeningElement(
                JSXOpeningElement.newBuilder()
                  .setName(Node.newBuilder().setType(NodeType.JSXIdentifierType).build())
                  .setSelfClosing(true)
                  .build()
              )
              .build()
          )
          .build()
      )
      .build();
    JSXFragment jsxFragment = JSXFragment.newBuilder()
      .setOpeningFragment(openingFragment)
      .setClosingFragment(closingFragment)
      .addChildren(childElement)
      .addChildren(childElement)
      .build();
    Node protobufNode = Node.newBuilder()
      .setType(NodeType.JSXFragmentType)
      .setJSXFragment(jsxFragment)
      .build();

    ESTree.Node estree = ESTreeFactory.from(protobufNode, ESTree.Node.class);
    assertThat(estree).isInstanceOfSatisfying(ESTree.JSXFragment.class, jsx -> {
      assertThat(jsx.openingFragment()).isInstanceOf(ESTree.JSXOpeningFragment.class);
      assertThat(jsx.closingFragment()).isInstanceOf(ESTree.JSXClosingFragment.class);
      assertThat(jsx.children()).hasSize(2);
      assertThat(jsx.children().get(0)).isInstanceOf(ESTree.JSXElement.class);
      assertThat(jsx.children().get(1)).isInstanceOf(ESTree.JSXElement.class);
    });
  }
}
